package Coriander::World;
use strict;
use warnings;
use YAML qw(DumpFile LoadFile);
use File::Basename 'basename';

# A class to load world setting files, extract settings from them, set settings, and save everything back to those files.

# This tiny gem will hold meta data about world objects, such as file names, last modification time, a "dirty" flag (a setting has been changed, but the file hasn't been saved since), a "flush" flag, et cetera. It uses the Inside Out Object technique, so that the actual world object only contain their data, not their meta data.
my %meta = ();

# $world = Coriander::World->new();
# Creates a new world object, ready to be used.
sub new {
    my $class = shift;
    my $file = "$Coriander::appdir/worlds/world%s.yaml";
    my $count = -1;
    $count-- while -e sprintf($file,$count);
    my $self = {
        name    => "New World",
        host    => "",
        port    => "",
        characters => {},
    };
    
    bless $self, $class;
    $meta{$self}->{file} = sprintf($file,$count);
    $meta{$self}->{timestamp} = time;
    return $self;
}

# $world = Coriander::World->load( $filename );
# Loads a world file into a world object.
sub load {
    my $class = shift;
    my $file = shift;
    print "Loading $file...\n";
    $file = "$Coriander::appdir/worlds/$file";
    my $self = LoadFile $file;
    bless $self, $class;
    $meta{$self}->{file} = $file;
    $meta{$self}->{timestamp} = (stat $file)[9];
    return $self;
}

# $world->delete();
# Delete a world file associated with a world object.
sub delete {
    my $self = shift;
    print "Deleting ", $meta{$self}->{file}, "...\n";
    unlink $meta{$self}->{file};
    delete $meta{$self};
}

# @worlds = Coriander::World::loadall();
# Load all world files into world objects. Returns a list containing those objects, ordered by their last-modified time.
sub loadAll {
    my @worlds = ();
    for my $file ( <"${Coriander::appdir}/worlds/*.yaml"> ) {
        push @worlds, Coriander::World->load(basename($file));
    }
    return sort {$meta{$b}->{timestamp} <=> $meta{$a}->{timestamp}} @worlds; 
}

# $world->save;
# Save a world object to its associated file.
sub save {
    my $self = shift;
    return if not exists $meta{$self};  # No need in saving this world if we don't have its meta data around!
                                        # Missing meta data strongly suggests we $world->delete()d it anyway.
    print "Saving $meta{$self}->{file}...\n";
    if ( not -e "$Coriander::appdir/worlds") {
        mkdir "$Coriander::appdir/worlds";
    }
    DumpFile $meta{$self}->{file}, {%{$self}};
    delete $meta{$self}->{dirty};
}

# $world->set( $key, $newvalue );
# Sets a world setting.
# $key can be a period-separated path of hashes-of-hashes to reach into nested data structures, if so needed.
# If $newvalue is undef, the setting is deleted from the world object.
sub set {
    my $self = shift;
    my $key = shift;
    my $value = shift;
    
    my ($data, $path, $localkey) = $self->_parsePath($key, 1);
    
    # Don't store an undef $value, don't store if $key already has that $value.
    if (defined($value) && (exists $data->{$localkey} && $data->{$localkey} ne $value)) {
        $data->{$localkey} = $value;
        $meta{$self}->{dirty} = 1;
    # Delete $key if $value is undef.
    } elsif (not defined($value) && exists $data->{$localkey}) {
        # And delete recursively, so we don't keep empty hashes sticking around.
        while (not keys %$data) {
            delete $data->{$localkey};
            ($data, $path, $localkey) = $self->_parsePath($path);
            last if not $path;
        }
        $meta{$self}->{dirty} = 1;
    }
        
    # Save the world object unless we've set a "flush" flag for this object.
    if ( (not defined $meta{$self}->{flush}) || $meta{$self}->{flush} ) {
        $self->save;
    }
}
# $world->meta( $key => $value); # Set meta data for the world object
# $value = $world->meta( $key ); # Retrieve meta data
sub meta {
    my $self = shift;
    my $key = shift;
    if (@_) {
        $meta{$self}->{$key} = shift;
    }
    return $meta{$self}->{$key};
}


# $value = $world->get( $key );
# Extracts the value of a setting for this world object.
# $key can be a period-separated path of hashes-of-hashes to reach into nested data structures, if so needed.
sub get {
    my $self = shift;
    my $key = shift;
    
    my ($data, $path, $localkey) = $self->_parsePath($key, 0);
    if (defined $localkey) {
        return $data->{$localkey};
    } else {
        return;
    }
}

# $world->flush( 0 );   # Disable auto-flushing for this world.
# $world->flush( 1 );   # Re-enable auto-flushing.
sub flush {
    my $self = shift;
    $meta{$self}->{flush} = $_[0];
    delete $meta{$self}->{flush} if $_[0];
}
    
# An internal method that parses a $key for set() and get() and dives right into the world object's internal data structure to get to the requested data.
sub _parsePath {
    my ($self, $key, $create) = @_;
    my $data = $self;
    my @path = split(/\./, $key);
    my $localkey = pop @path;
    
    for my $k (@path) {
        if ( not exists $data->{$k} ) {
            if ($create) {
                $data->{$k} = {};
            } else {
                return ($self, undef);
            }
        }
            
        my $name = $k;
        my $count = "";
        while ( ref($data->{$k}) ne "HASH" ) {
            $count++;
            $k = sprintf "%s~%s", $name, $count;
        };
        $data = $data->{$k};
    }
    return ($data, join(".", @path), $localkey);
}
        
# If needed, save the world object when it gets destroyed.       
sub DESTROY {
    $_[0]->save if exists $meta{$_[0]} && -e "$Coriander::appdir/worlds/" . $meta{$_[0]}->{file};
    delete $meta{$_[0]};
}
    
1;
        